import WebpackLicense from '@components/WebpackLicense';

<WebpackLicense from="https://webpack.js.org/api/loaders/#examples" />

# Writing loaders

Learn how to create custom loaders for Rspack to transform files.

## Loader types

Rspack supports multiple loader types, including sync loader, async loader, ESM loader, Raw loader, and Pitching loader.

The following sections provide some basic examples of the different types of loaders.

### Sync loader

Sync loaders are the most basic type of loader. They can synchronously return transformed content using either a `return` statement or the `this.callback` method:

```js title="sync-loader.js"
module.exports = function (source, map, meta) {
  return someSyncOperation(source);
};
```

Compared to the `return` statement, the `this.callback` method offers more flexibility as it allows passing multiple parameters, including error information, source maps, and metadata:

```js title="sync-loader-with-callback.js"
module.exports = function (source, map, meta) {
  this.callback(null, someSyncOperation(source), map, meta);

  // Return undefined when calling callback() to avoid return value conflicts
  return;
};
```

::: info

- The `map` and `meta` parameters are optional, see [this.callback](/api/loader-api/context#thiscallback) and [Handling source maps](#handling-source-maps) for more details.
- Rspack will internally convert loaders to async, regardless of whether it's a synchronous loader, for technical and performance reasons.

:::

### Async loader

When you need to perform async operations (such as file I/O, network requests, etc.), you should use an async loader. Call the [this.async()](/api/loader-api/context#thisasync) method to get a `callback` function, informing Rspack that this loader requires async processing.

The `callback` of an async loader can also pass multiple parameters, including transformed content, source maps, and metadata:

```js title="async-loader.js"
module.exports = function (source, map, meta) {
  // Get the async callback function
  const callback = this.async();

  // Perform async operation
  someAsyncOperation(source, function (err, result) {
    // Handle error case
    if (err) return callback(err);

    // Return the processing result on success
    callback(null, result, map, meta);
  });
};
```

### ESM loader

Rspack supports ESM loaders, you can write loaders using ESM syntax and export the loader function using `export default`.

When writing ESM loaders, the file name needs to end with `.mjs`, or set `type` to `module` in the nearest `package.json`.

```js title="my-loader.mjs"
export default function loader(source, map, meta) {
  // ...
}
```

If you need to export options like [raw](#raw-loader) or [pitch](#pitching-loader), you can use named exports:

```js title="my-loader.mjs"
export default function loader(source) {
  // ...
}

// Set raw loader
export const raw = true;

// Add pitch function
export function pitch(remainingRequest, precedingRequest, data) {
  // ...
}
```

::: tip
ESM loader and CommonJS loader have the same functionality, but use different module syntax. You can choose the format based on your project needs.
:::

### Raw loader

By default, Rspack converts file content into UTF-8 strings before passing it to loaders for processing. However, when handling binary files (such as images, audio, or font files), we need to work directly with the raw binary data rather than string representations.

By exporting `raw: true` in the loader file, a loader can receive the original `Buffer` object as input instead of a string.

- CJS:

```js title="raw-loader.js"
  // Process binary content
  // Here source is a Buffer instance
  const processed = someBufferOperation(source);

  // Return the processed result
  return processed;
};

// Mark as Raw Loader
module.exports.raw = true;
```

- ESM:

```js title="raw-loader.mjs"
export default function loader(source) {
  // ...
}

export const raw = true;
```

When multiple loaders are chained together, each loader can choose to receive and pass processing results as either strings or Buffers. Rspack automatically handles the conversion between Buffer and string between different loaders, ensuring data is correctly passed to the next loader.

Raw Loaders are particularly useful in scenarios involving image compression, binary resource transformation, file encoding, etc. For example, when developing a loader to process images, direct manipulation of binary data is typically required to properly handle image formats.

### Pitching loader

In Rspack's loader execution process, the default exported loader function is always called **from right to left** (called normal stage). However, sometimes a loader may only care about the request's metadata rather than the processing result of the previous loader. To address this need, Rspack provides a pitching stage — a special stage that each loader can define before its normal execution.

Contrary to normal execution, the `pitch` method exported in the loader file is called **from left to right**, before any loader's default function executes. This bidirectional processing mechanism provides developers with more flexible resource handling options.

For example, with the following configuration:

```js title="rspack.config.mjs"
export default {
  //...
  module: {
    rules: [
      {
        //...
        use: ['a-loader', 'b-loader', 'c-loader'],
      },
    ],
  },
};
```

These steps would occur:

```
|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
      |- requested module is picked up as a dependency
    |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution
```

Normally, if it the loader is simple enough which only exports the normal stage hook:

```js
module.exports = function (source) {};
```

Then, the pitching stage will be skipped.

So why might a loader take advantage of the pitching stage?

First, the data passed to the pitch method is exposed in the execution stage as well under this.data and could be useful for capturing and sharing information from earlier in the cycle.

```js
module.exports = function (source) {
  return someSyncOperation(source, this.data.value);
};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  data.value = 42;
};
```

Second, if a loader delivers a result in the pitch method, the process turns around and skips the remaining loaders.
In our example above, if the b-loaders pitch method returned something:

```js
module.exports = function (source) {
  return someSyncOperation(source);
};

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  if (someCondition()) {
    return (
      'module.exports = require(' +
      JSON.stringify('-!' + remainingRequest) +
      ');'
    );
  }
};
```

The steps above would be shortened to:

```
|- a-loader `pitch`
  |- b-loader `pitch` returns a module
|- a-loader normal execution
```

For a real world example, `style-loader` leverages the second advantage to dispatch requests.
Please visit [style-loader](https://github.com/webpack/style-loader/blob/eb06baeb3ac4e3107732a21170b0a7f358c5423f/src/index.js#L39) for details.

## Write with TypeScript

If you write Rspack loader using TypeScript, you can use `LoaderDefinition` to provide complete type definitions for your loader.

```ts title="my-loader.ts"
import type { LoaderDefinition } from '@rspack/core';

// Declare the type of loader options
type MyLoaderOptions = {
  foo: string;
};

const myLoader: LoaderDefinition<MyLoaderOptions> = function (source) {
  const options = this.getOptions();
  console.log(options); // { foo: 'bar' }
  // Transform the source code
  return `// Processed by my-loader\n${source}`;
};

export default myLoader;
```

Alternatively, you can import `LoaderContext` to add types to the loader context:

```ts title="my-loader.ts"
import type { LoaderContext } from '@rspack/core';

type MyLoaderOptions = {
  foo: string;
};

export default function myLoader(
  this: LoaderContext<MyLoaderOptions>,
  source: string,
) {
  const options = this.getOptions();
  console.log(options); // { foo: 'bar' }
  return `// Processed by my-loader\n${source}`;
}
```

## Handling source maps

When developing loaders, properly handling source maps is essential for providing accurate debugging information.

Loader APIs related to source maps:

- Use [this.sourceMap](/api/loader-api/context#thissourcemap) to determine whether source map generation is required.
- Use [this.callback](/api/loader-api/context#thiscallback) or [this.async](/api/loader-api/context#thisasync) methods to return the processed source map.

### Automatic source map handling

Some transformers support automatic source map handling. For example, SWC can automatically generate new source maps based on input source maps.

Here's an example using Rspack's [SWC API](/api/javascript-api/swc):

```js title="myLoader.js"
import { rspack } from '@rspack/core';

const { swc } = rspack.experiments;

export default async function myLoader(source, inputSourceMap) {
  const callback = this.async();

  try {
    const result = await swc.transform(source, {
      inputSourceMap,
      sourceMaps: this.sourceMap,
      // ...other options
    });

    callback(null, result.code, this.sourceMap ? result.map : undefined);
  } catch (err) {
    callback(err);
  }
}
```

### Manual source map merging

You can also manually merge multiple source maps using third-party libraries such as [@jridgewell/remapping](https://www.npmjs.com/package/@jridgewell/remapping):

```js title="myLoader.js"
import remapping from '@jridgewell/remapping';

const mergeSourceMap = (inputSourceMap, newSourceMap) => {
  return remapping([newSourceMap, inputSourceMap], () => null);
};

export default async function myLoader(source, inputSourceMap) {
  const callback = this.async();

  try {
    const { code, sourceMap: newSourceMap } = await someTransformFunction(
      source,
      { sourceMap: this.sourceMap },
    );

    if (this.sourceMap) {
      const mergedSourceMap = inputSourceMap
        ? mergeSourceMap(inputSourceMap, newSourceMap)
        : newSourceMap;
      callback(null, code, mergedSourceMap);
    } else {
      callback(null, code);
    }
  } catch (err) {
    callback(err);
  }
}
```
